Windows核心编程 读书笔记(2)

进/线程同步

用户模式下的线程同步

可以理解为同一进程内的线程同步, 如果不涉及跨进程, 使用这些函数同步效率更高.

原子访问 Interlocked

对标C++的atomic, 可以实现完全替代, C++atomic在windows上运行底层用的就是这个.

关键段 critical_section

对标C++的mutex, 基本上完全替代, 除了critical_section可以通过EnterCriticalSection获取线程访问资源(是否有线程访问资源, 哪个线程在访问资源等).

内部逻辑都是先进行一段时间的自旋, 如果仍未解锁则切换到内核模式进入等待.

读写锁 SRWLock

对标C++的shared_mutex, 可以实现完全替代.

由于本人对于shared_mutex使用比较少, 这里复习一下使用方式 :

  • 简单说就是读写锁在读时用shared_lock锁, 在写时用unique_lock锁.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <shared_mutex>
    #include <vector>

    std::shared_mutex smtx;
    std::vector<int> data;

    // 读线程(多个线程可同时执行)
    void ReadData() {
    std::shared_lock<std::shared_mutex> lock(smtx); // 读锁
    for (int val : data) {
    std::cout << val << " ";
    }
    } // lock 自动释放

    // 写线程(独占访问)
    void WriteData(int value) {
    std::unique_lock<std::shared_mutex> lock(smtx); // 写锁
    data.push_back(value);
    } // lock 自动释放
    • 延迟加锁(defer_lock):

      不是创建即加锁, 可以做些条件判断后再手动加.

      1
      2
      3
      4
      5
      std::shared_lock<std::shared_mutex> lock(smtx, std::defer_lock);
      if (/* 检查条件 */) {
      lock.lock(); // 手动加锁
      // 读操作...
      }
    • 尝试加锁(try_lock):

      非阻塞尝试获取读锁,失败时立即返回.

      1
      2
      3
      4
      5
      6
      std::shared_lock<std::shared_mutex> lock(smtx, std::try_to_lock);
      if (lock.owns_lock()) { // 检查是否加锁成功
      // 读操作...
      } else {
      // 执行其他逻辑
      }
    • 超时加锁(try_lock_for/try_lock_until

      在指定时间内尝试获取读锁.

      1
      2
      3
      4
      5
      using namespace std::chrono_literals;
      std::shared_lock<std::shared_mutex> lock(smtx, 100ms); // 等待最多 100ms
      if (lock.owns_lock()) {
      // 读操作...
      }
  • 还可以和条件变量结合 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    std::shared_mutex smtx;
    std::condition_variable_any cv;
    bool data_ready = false;

    // 读线程等待条件
    void WaitForData() {
    std::shared_lock<std::shared_mutex> lock(smtx);
    cv.wait(lock, [] { return data_ready; }); // 自动释放锁并等待
    // 数据已就绪,继续读操作...
    }

    // 写线程通知条件
    void NotifyDataReady() {
    {
    std::unique_lock<std::shared_mutex> lock(smtx);
    data_ready = true;
    }
    cv.notify_all(); // 唤醒所有等待线程
    }

条件变量 SleepConditionVariable

对标C++的condition_variable, 可以完全替代.

锁使用

  • 代码中任何地方都要以完全相同的顺序获取锁.
  • 在锁区域中尽量避免调用高消耗函数, 假如只是希望投放到函数中的某些共享资源不要被修改, 那么可以先在锁区域中拷贝(C++有移动拷贝, 如果想直接把资源夺过来, 直接移动即可), 然后解锁放入函数执行, 如果有恢复的需要再swap回去就行了, 这会比整个函数加锁好.

用内核对象进行线程同步

这种同步使用于两种情况 :

  • 跨进程同步
  • 和内核对象强关联的同步

等待函数

共两种 :

  • WaitForSingleObject : 等待单个内核对象.

    1
    2
    3
    4
    DWORD WaitForSingleObject(
    HANDLE hHandle, // 事件句柄
    DWORD dwMilliseconds // 超时时间(INFINITE表示无限等待)
    );
  • WaitForMultipleObjects : 等待多个内核对象.

    1
    2
    3
    4
    5
    6
    DWORD WaitForMultipleObjects(
    DWORD nCount, // 对象数量
    const HANDLE *lpHandles, // 对象句柄数组
    BOOL bWaitAll, // TRUE=等待所有,FALSE=等待任意一个
    DWORD dwMilliseconds // 超时时间
    );

使用方法非常简单, 传入 内核句柄 + 时间 即可, Multi版本传入的内核句柄数组, 还有一个标志位bWaitAll, 该标志位传入false表示有一个触发就继续, 传入true表示所有触发才能继续.

如果仅限于进线程同步的使用, 使用方法和join差不多, 都是目标进线程都结束就触发.

触发条件

  • 所有内核对象都会存在一种信号机制, 无信号表示未触发, 有信号表示触发.
  • 不同的内核对象都有不同的信号触发情况, 最终目的都是为了根据对象的某些变化做出对应的反应.

下面是常见内核对象的触发条件 :

内核对象 常见创建函数 无信号状态 (等待原因) 有信号状态 (触发条件)
线程 (Thread) CreateThread 线程正在运行 线程终止
进程 (Process) CreateProcess 进程正在运行 进程终止
互斥体 (Mutex) CreateMutex 被线程拥有 (Wait后未Release) 不被任何线程拥有(已释放)
事件 (Event) CreateEvent 手动重置:需要显式调用 SetEvent 自动重置:需要显式调用 SetEvent 手动重置SetEvent设置,ResetEvent手动重置 自动重置SetEvent设置,系统自动重置(唤醒一个线程后即复位)
信号量 (Semaphore) CreateSemaphore 计数器 == 0 计数器 > 0ReleaseSemaphore增加计数)
等待定时器 (Waitable Timer) CreateWaitableTimer 未到设定时间 到达设定时间SetWaitableTimer
作业对象 (Job Object) CreateJobObject 作业中的所有进程尚未全部终止 作业中的所有进程已全部终止
文件变更通知 FindFirstChangeNotification 指定目录/文件未发生变更 指定目录/文件发生了变更

事件对象

你可以认为这是一个配合等待函数实现事件触发的内核对象.

  • 事件对象的信号有无是可以被随意设置的.

  • 所谓”事件”, 你可以认为是在某些条件具备后可以执行的任务. 通常情况下是一个进/线程持有事件句柄调用等待函数, 在其他线程将某些条件准备好后触发事件句柄, 让该进程脱离等待.

  • 事件对象的独特优势在于可以跨进程通知一次性通知多个等待者.

  • SetEvent :

    将事件句柄传入该函数, 即可立即触发该对象.

    1
    BOOL SetEvent(HANDLE hEvent);
  • CreateEvent :

    1
    2
    3
    4
    5
    6
    HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性,通常NULL
    BOOL bManualReset, // 手动/自动重置 TRUE/FALSE
    BOOL bInitialState, // 初始状态:TRUE(有信号)/FALSE(无信号)
    LPCSTR lpName // <- 事件对象的名字(关键参数!)
    );
    • bManualReset : 这里所谓的手动/自动重置(TRUE/FALSE), 其实就是在被SetEvent触发后是否重置为无信号. 实际我们会更关注其副现象 : 前者被SetEvent触发会直接唤醒所有等待线程, 而后者只会唤醒一个, 类似于notice_allnotice_one的区别.
  • OpenEvent :

    用于打开一个已有的事件句柄.

    1
    2
    3
    4
    5
    HANDLE OpenEvent(
    DWORD dwDesiredAccess, // 访问权限(如EVENT_ALL_ACCESS)
    BOOL bInheritHandle, // 是否可被子进程继承
    LPCSTR lpName // 事件名称
    );

    关键在于传入的名字, 我们在CreateEvent中有设置, 只要名字相同, 就可以在不同进程拥有同一个事件句柄, 这是跨进程通信的关键. 因此如果想进行跨进程通信, 保证事件名称相同即可, 可以在规定中记录, 也可以注册到注册表中, 多个进程查表拿名字也行.

  • ResetEvent : 将已触发的句柄设置为未触发.

信号量

用于维护资源数量的进程间同步方式.

  • CreateSemaphore/ CreateSemaphoreEx: 创建或打开一个命名/匿名信号量。
  • OpenSemaphore: 打开一个已存在的命名信号量。
  • ReleaseSemaphore: 增加信号量的计数,释放资源。
  • WaitForSingleObject/ WaitForMultipleObjects: 用于等待(获取)信号量。

互斥量

如果是需要跨进程通信, 可以使用该互斥量, 不然乖乖用std::mutex就行.

  • 该互斥锁具有“所有权”概念。如果一个线程持有内核互斥锁时终止,系统会将其视为已释放,并唤醒等待队列中的一个线程。

MsgWaitForMultipleObjectsEx

这个函数在WaitForMultipleObjectsEx的基础上, 可以关心一些额外的事件(比如键盘和鼠标), 当这些事件触发后也会退出等待. 这种方式可以防止UI界面卡死.

1
2
3
4
5
6
7
DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount, // 要等待的内核对象数量(最多 MAXIMUM_WAIT_OBJECTS)
const HANDLE *pHandles, // 内核对象句柄数组(如事件、线程、信号量等)
DWORD dwMilliseconds, // 超时时间(毫秒),INFINITE 表示无限等待
DWORD dwWakeMask, // 要等待的消息类型(如 QS_ALLINPUT)
DWORD dwFlags // 控制等待行为的标志(如 MWMO_ALERTABLE)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while(true)
{
DWORD dwResult = MsgWaitForMultipleObjectsEx(
1, &hEvent, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE
);

if (dwResult == WAIT_OBJECT_0) {
// 事件触发
}
else if (dwResult == WAIT_OBJECT_0 + 1) {
// 处理消息
}
else if (dwResult == WAIT_IO_COMPLETION) {
// APC 回调触发(可强制退出等待)
}
}

不过现代C++也有更简单的替代方案(future + async) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto future = std::async(std::launch::async, [] {
return DoLongRunningTask();
});

while (true) {
// 非阻塞检查任务是否完成
if (future.wait_for(std::chrono::milliseconds(100)) == std::future_status::ready) {
break;
}

// 处理消息(模拟消息泵)
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
DispatchMessage(&msg);
}
}

Windows核心编程 读书笔记(2)
http://example.com/2025/09/17/[Windows核心编程]读书笔记(2)/
作者
天目中云
发布于
2025年9月17日
许可协议